package com.kapcb.log.filter;

import com.kapcb.log.properties.LogProperties;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.LocalDateTime;

/**
 * <a>Title: RequestLoggingFilter </a>
 * <a>Author: kapcb <a>
 * <a>Description: RequestLoggingFilter <a>
 * 1、AOP 拦截所有方法，可以拦截指定 Controller
 * 2、Filter 拦截所有请求，拦截位置比 Handler 靠前，比较适合用于行为日志记录
 * 以终为始，不管黑猫白猫，能抓住老鼠就是好猫。这两种方式均能满足需求。这里选用 Filter 方式实现
 * <p>
 * OncePerRequestFilter 和 AbstractRequestLoggingFilter 都可实现
 * 抽象类 OncePerRequestFilter 继承自 GenericFilterBean，保留了 GenericFilterBean 中的所有方法并对之进行了扩展，在 OncePerRequestFilter 中的主要方法是 doFilter
 * AbstractRequestLoggingFilter是对 OncePerRequestFilter 的扩展，它除了遗传了其父类及祖先类的所有功能外，还在 doFilterInternal 中决定了在过滤之前和之后执行的事件，它的子类关注的是 beforeRequest 和 afterRequest
 *
 * @author Kpacb
 * @version 1.0
 * @date 2024/5/25 10:06
 * @since 1.0
 */
@Slf4j
public class RequestLoggingFilter extends OncePerRequestFilter {

    private final LogProperties logProperties;

    public RequestLoggingFilter(LogProperties logProperties) {
        this.logProperties = logProperties;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 默认的 HttpServletRequest 和 HttpServletResponse 中的流被读取一次之后，再次读取会失败
        boolean isFirstRequest = !isAsyncDispatch(request);
        String requestMethod = request.getMethod();
        HttpServletRequest requestToUse = request;
        HttpServletResponse responseToUse = response;

        // 一定要先进行一步判断。也就是 request instanceof ContentCachingRequestWrapper 和 response instanceof ContentCachingResponseWrapper
        if (isFirstRequest && !(request instanceof ContentCachingRequestWrapper) && (HttpMethod.POST.matches(requestMethod) || HttpMethod.PUT.matches(requestMethod))) {
            // 使用 ContentCachingRequestWrapper 和 ContentCachingResponseWrapper 进行包装，实现重复读取
            requestToUse = new ContentCachingRequestWrapper(request, this.logProperties.getMaxPayloadLength());
        }

        if (!(response instanceof ContentCachingResponseWrapper) && (HttpMethod.POST.matches(requestMethod) || HttpMethod.PUT.matches(requestMethod))) {
            responseToUse = new ContentCachingResponseWrapper(response);
        }

        int status = HttpStatus.INTERNAL_SERVER_ERROR.value();
        LocalDateTime start = LocalDateTime.now();

        try {
            filterChain.doFilter(requestToUse, responseToUse);
            status = response.getStatus();
        } finally {
            if (logProperties.isEnable() && !logProperties.getIgnoreContentTypes().contains(request.getContentType())) {
                if (!isAsyncStarted(requestToUse) && (HttpStatus.OK.value() == status)) {
                    LocalDateTime end = LocalDateTime.now();
                    Duration duration = Duration.between(start, end);
                    long useTime = duration.toMillis();
                    log.info("request : {}", createMessage(requestToUse));
                    log.info("response : {}, cost : {} ms", createResponseMessage(responseToUse), useTime);
                }
            }
            writeResponseBack(responseToUse);
        }
    }

    /**
     * request.getInputStream() 获取请求 body里 面的内容只能被获取一次
     * ContentCachingRequestWrapper 通过这个类能够解决解决 HttpServletRequest inputStream 只能读取一次的问题
     * ContentCachingRequestWrapper 是 Spring Boot 自带的请求体重复利用的工具，不过也有缺点
     * That means if the request:
     * 1、content is not consumed, then the content is not cached
     * 2、and cannot be retrieved via {@link ContentCachingRequestWrapper#getContentAsByteArray()} ()}
     * 也就是说，使用 ContentCachingRequestWrapper的前提:
     * 1、你要消费这个请求数据
     * 2、能过 getContentAsByteArray 方法获取缓存内容
     *
     * @param request
     * @return
     */
    private String getMessagePayload(HttpServletRequest request) {
        String requestBody = "";
        ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
        if (wrapper != null) {
            byte[] buf = wrapper.getContentAsByteArray();
            String characterEncoding = StandardCharsets.ISO_8859_1.name().equals(wrapper.getCharacterEncoding()) ? StandardCharsets.UTF_8.name() : wrapper.getCharacterEncoding();
            requestBody = genPayload(requestBody, buf, characterEncoding);
        }
        return requestBody;
    }

    private String getMessageResponseBody(HttpServletResponse response) {
        String responseBody = "";
        ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
        if (wrapper != null && !checkContentType(wrapper, MediaType.APPLICATION_OCTET_STREAM_VALUE)) {
            byte[] buf = wrapper.getContentAsByteArray();
            try {
                wrapper.copyBodyToResponse();
            } catch (Exception e) {
                log.error("get response body error");
            }
            String characterEncoding = StandardCharsets.ISO_8859_1.name().equals(wrapper.getCharacterEncoding()) ? StandardCharsets.UTF_8.name() : wrapper.getCharacterEncoding();
            responseBody = genPayload(responseBody, buf, characterEncoding);
        }
        return responseBody;
    }

    /**
     * 解决字节流在读取了一次之后清空的问题
     */
    private void writeResponseBack(HttpServletResponse response) {
        ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
        if (wrapper != null) {
            try {
                wrapper.copyBodyToResponse();
            } catch (IOException e) {
                log.error("Cannot copy Fail to write response body back", e);
            }
        }
    }

    private String genPayload(String payload, byte[] buf, String characterEncoding) {
        if (buf.length > 0 && buf.length < logProperties.getMaxPayloadLength()) {
            try {
                payload = new String(buf, 0, buf.length, characterEncoding);
            } catch (UnsupportedEncodingException ex) {
                payload = "[unknown]";
            }
        }
        return payload;
    }

    private String createMessage(HttpServletRequest request) {

        StringBuilder message = new StringBuilder();
        message.append(request.getMethod()).append(" ");
        message.append(request.getRequestURI());

        String queryString = request.getQueryString();
        if (StringUtils.isNotBlank(queryString)) {
            message.append('?').append(queryString);
        }

        String remoteAddr = request.getRemoteAddr();
        if (StringUtils.isNotBlank(remoteAddr)) {
            message.append(", from=").append(remoteAddr);
        }

        HttpSession session = request.getSession(false);
        if (session != null) {
            message.append(", session=").append(session.getId());
        }

        String remoteUser = request.getRemoteUser();
        if (StringUtils.isNotBlank(remoteUser)) {
            message.append(", user=").append(remoteUser);
        }

        HttpHeaders headers = new ServletServerHttpRequest(request).getHeaders();
        message.append(", headers=").append(headers);

        String messagePayload = this.getMessagePayload(request);
        if (StringUtils.isNotBlank(messagePayload)) {
            message.append(", payload=").append(messagePayload);
        }
        return message.toString();
    }

    private String createResponseMessage(HttpServletResponse response) {
        StringBuilder message = new StringBuilder();
        String responseBody = getMessageResponseBody(response);
        if (StringUtils.isNotBlank(responseBody)) {
            message.append("body=").append(responseBody);
        }
        return message.toString();
    }

    private boolean checkContentType(HttpServletResponse response, String mediaType) {
        String contentType = response.getContentType();
        return contentType != null && contentType.contains(mediaType);
    }

}
